﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Threading;

namespace KeyCtrl
{
    class KeyDaemon
    {
        [DllImport("user32.dll", SetLastError = true)]
        protected static extern IntPtr SetWindowsHookEx(HookType code, HookProc func, IntPtr hInstance, int threadID);
        [DllImport("user32.dll")]
        static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);
        [DllImport("user32.dll", SetLastError = true)]
        static extern bool UnhookWindowsHookEx(IntPtr hhk);
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

        public delegate int HookProc(int code, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);

        public enum HookType : int
        {
            WH_JOURNALRECORD = 0,
            WH_JOURNALPLAYBACK = 1,
            WH_KEYBOARD = 2,
            WH_GETMESSAGE = 3,
            WH_CALLWNDPROC = 4,
            WH_CBT = 5,
            WH_SYSMSGFILTER = 6,
            WH_MOUSE = 7,
            WH_HARDWARE = 8,
            WH_DEBUG = 9,
            WH_SHELL = 10,
            WH_FOREGROUNDIDLE = 11,
            WH_CALLWNDPROCRET = 12,
            WH_KEYBOARD_LL = 13,
            WH_MOUSE_LL = 14
        }

        public struct KBDLLHOOKSTRUCT
        {
            public int vkCode;
            int scanCode;
            public int flags;
            int time;
            int dwExtraInfo;
        }

        private HookProc _myCallbackDelegate = null;
        private IntPtr _hookHandle;
        private Boolean _isLogging;
        private event EventHandler _eventHandler;
        public AccumulativeKeyCombo PressedKeys;
        public Boolean Block = false; //whether or not to block keys from going into other applications

        public KeyDaemon()
        {
            PressedKeys = new AccumulativeKeyCombo();
            _isLogging = false;
            _eventHandler = null;
        }

        ~KeyDaemon()
        {
            StopLogging();
        }

        public void StartLogging()
        {
            if (!_isLogging)
            {
                Logger.Log("Key Daemon: StartLogging");

                //SetWindowsHookEx(HookType.WH_KEYBOARD, this._myCallbackDelegate, new IntPtr(AppDomain.GetCurrentThreadId()), 0);
                //int i = Marshal.GetLastWin32Error();
                //_eventThread = new System.Threading.Thread(ExecuteEventHandler);
                this._myCallbackDelegate = new HookProc(this.MyCallbackFunction);
                BGException.Assert(
                    (_hookHandle = SetWindowsHookEx(
                        HookType.WH_KEYBOARD_LL,
                        this._myCallbackDelegate,
                        GetModuleHandle(System.Diagnostics.Process.GetCurrentProcess().MainModule.ModuleName),
                        0
                    )).ToInt32() != 0,
                    "Can't create keyboard hook. SetWindowsHookEx returned 0, GetLastError()=" + Marshal.GetLastWin32Error());

                _isLogging = true;
            }
        }

        public void StopLogging()
        {
            if (_isLogging)
            {
                Logger.Log("Key Daemon: StopLogging");
                //I don't think assertation is needed here because what might happen is that the thread will die therefore windows will close this handle
                //And only then will the GC kick in therefore we might get an error.

                UnhookWindowsHookEx(_hookHandle);
                //BGException.Assert(, "Unhook failed, get last error returned:" + Marshal.GetLastWin32Error());
                _isLogging = false;
            }
        }

        public void SetEventHandler(EventHandler handler)
        {
            _eventHandler = handler;
        }

        private void ExecuteEventHandler()
        {
            if (_eventHandler != null)
                _eventHandler(this, new EventArgs());
        }

        private int MyCallbackFunction(int code, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam)
        {
            //you need to call CallNextHookEx without further processing
            //and return the value returned by CallNextHookEx

            if (code < 0)
                return CallNextHookEx(IntPtr.Zero, code, wParam, ref lParam);
            
            int ret;
            if (Block) //This block should be before the filter code 
                ret = 1; //so that if block = false the key will be passed before processing so not to lag the OS and other apps
            else
                ret = CallNextHookEx(IntPtr.Zero, code, wParam, ref lParam);

            // we can convert the 2nd parameter (the key code) to a System.Windows.Forms.Keys enum constant

            Keys key = (Keys)Enum.Parse(typeof(Keys), lParam.vkCode.ToString());
            if ((lParam.flags == 0) || (wParam == (IntPtr)0x0104))
            {
                if (PressedKeys.KeyDown(key))
                    ExecuteEventHandler();
            }
            else
            {
                PressedKeys.KeyUp(key);
                ExecuteEventHandler();
            }

            //return the value returned by CallNextHookEx
            return ret;
        }
    }
}
